To instruct the BinaryOp delegate to invoke Add() asynchronously, you will modify the logic in the previous project (feel free to add code to the existing project, however in your lab downloads, you will find a new Console Application named AsyncDelegate). Update the previous Main() method as follows:
static void Main(string[] args) { Console.WriteLine("***** Async Delegate Invocation *****"); // Print out the ID of the executing thread. Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); // Invoke Add() on a secondary thread. BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null); // Do other work on primary thread... Console.WriteLine("Doing more work in Main()!"); // Obtain the result of the Add() // method when ready. int answer = b.EndInvoke(iftAR); Console.WriteLine("10 + 10 is {0}.", answer); Console.ReadLine(); }
If you run this application, you will find that two unique thread IDs are displayed, given that there are in fact multiple threads working within the current AppDomain:
***** Async Delegate Invocation ***** Main() invoked on thread 1. Doing more work in Main()! Add() invoked on thread 3. 10 + 10 is 20.
In addition to the unique ID values, you will also notice upon running the application that the Doing more work in Main()! message displays immediately, while the secondary thread is occupied attending to its business.
If you think carefully about the current implementation of Main(), you might have realized that the time span between calling BeginInvoke() and EndInvoke() is clearly less than five seconds. Therefore, once Doing more work in Main()! prints to the console, the calling thread is now blocked and waiting for the secondary thread to complete before being able to obtain the result of the Add() method. Therefore, you are effectively making yet another synchronous call:
static void Main(string[] args) { ... BinaryOp b = new BinaryOp(Add); // Once the next statement is processed, // the calling thread is now blocked until // BeginInvoke() completes. IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null); // This call takes far less than five seconds! Console.WriteLine("Doing more work in Main()!"); int answer = b.EndInvoke(iftAR); ... }
Obviously, asynchronous delegates would lose their appeal if the calling thread had the potential of being blocked under various circumstances. To allow the calling thread to discover if the asynchronously invoked method has completed its work, the IAsyncResult interface provides the IsCompleted property. Using this member, the calling thread is able to determine whether the asynchronous call has indeed completed before calling EndInvoke().
If the method has not completed, IsCompleted returns false, and the calling thread is free to carry on its work. If IsCompleted returns true, the calling thread is able to obtain the result in the “least blocking manner” possible. Ponder the following update to the Main() method:
static void Main(string[] args) { ... BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null); // This message will keep printing until // the Add() method is finished. while(!iftAR.IsCompleted) { Console.WriteLine("Doing more work in Main()!"); Thread.Sleep(1000); } // Now we know the Add() method is complete. int answer = b.EndInvoke(iftAR); ... }
Here, you enter a loop that will continue processing the Console.WriteLine() statement until the secondary thread has completed. Once this has occurred, you can obtain the result of the Add() method knowing full well the method has indeed completed. The call to Thread.Sleep(1000) is not necessary for this particular application to function correctly; however, by forcing the primary thread to wait for approximately one second during each iteration, it prevents the same message from printing hundreds of times. Here is the output (your output may differ slightly, based on the speed of your machine and when threads come to life):
***** Async Delegate Invocation ***** Main() invoked on thread 1. Doing more work in Main()! Add() invoked on thread 3. Doing more work in Main()! Doing more work in Main()! Doing more work in Main()! Doing more work in Main()! Doing more work in Main()! 10 + 10 is 20.
In addition to the IsCompleted property, the IAsyncResult interface provides the AsyncWaitHandle property for more flexible waiting logic. This property returns an instance of the WaitHandle type, which exposes a method named WaitOne(). The benefit of WaitHandle.WaitOne() is that you can specify the maximum wait time. If the specified amount of time is exceeded, WaitOne() returns false. Ponder the following updated while loop, which no longer makes use of a call to Thread.Sleep():
while (!iftAR.AsyncWaitHandle.WaitOne(1000, true)) { Console.WriteLine("Doing more work in Main()!"); }
While these properties of IAsyncResult do provide a way to synchronize the calling thread, they are not the most efficient approach. In many ways, the IsCompleted property is much like a really annoying manager (or classmate) who is constantly asking, “Are you done yet?” Thankfully, delegates provide a number of additional (and more elegant) techniques to obtain the result of a method that has been called asynchronously.
Source Code The AsyncDelegate project is located under the Chapter 19 subdirectory.
Rather than polling a delegate to determine whether an asynchronously invoked method has completed, it would be more efficient to have the secondary thread inform the calling thread when the task is finished. When you wish to enable this behavior, you will need to supply an instance of the System.AsyncCallback delegate as a parameter to BeginInvoke(), which up until this point has been null. However, when you do supply an AsyncCallback object, the delegate will call the specified method automatically when the asynchronous call has completed.
Note The callback method will be called on the secondary thread not the primary thread. This has important implications when using threads within a graphical user interface (WPF or Windows Forms) as controls have thread-affinity, meaning they can only be manipulated by the thread which created them. You’ll see some examples of working the threads from a GUI later in this chapter during the examination of the Task Parallel Library (TPL).
Like any delegate, AsyncCallback can only invoke methods that match a specific pattern, which in this case is a method taking IAsyncResult as the sole parameter and returning nothing:
// Targets of AsyncCallback must match the following pattern. void MyAsyncCallbackMethod(IAsyncResult itfAR)
Assume you have another Console Application (AsyncCallbackDelegate ) making use of the BinaryOp delegate. This time, however, you will not poll the delegate to determine whether the Add() method has completed. Rather, you will define a static method named AddComplete() to receive the notification that the asynchronous invocation is finished. Also, this example makes use of a class level static bool field, which will be used to keep the primary thread in Main() running a task until the secondary thread is finished.
Note The use of this Boolean variable in this example is strictly speaking, not thread safe, as there are two different threads which have access to its value. This will be permissible for the current example; however, as a very good rule of thumb, you must ensure data that can be shared among multiple threads is locked down. You’ll see how to do so later in this chapter.
namespace AsyncCallbackDelegate { public delegate int BinaryOp(int x, int y); class Program { private static bool isDone = false; static void Main(string[] args) { Console.WriteLine("***** AsyncCallbackDelegate Example *****"); Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginInvoke(10, 10, new AsyncCallback(AddComplete), null); // Assume other work is performed here... while (!isDone) { Thread.Sleep(1000); Console.WriteLine("Working...."); } Console.ReadLine(); } static int Add(int x, int y) { Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); return x + y; } static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete"); isDone = true; } } }
Again, the static AddComplete() method will be invoked by the AsyncCallback delegate when the Add() method has completed. If you run this program, you can confirm that the secondary thread is the thread invoking the AddComplete() callback:
***** AsyncCallbackDelegate Example ***** Main() invoked on thread 1. Add() invoked on thread 3. Working.... Working.... Working.... Working.... Working.... AddComplete() invoked on thread 3. Your addition complete
Like other examples in this chapter, your output may be slightly different. In fact, you may see one final “Working…” printout occur after the addition is complete. This is just a byproduct of the forced 1- second delay in Main().
Currently, the AddComplete() method is not printing out the actual result of the operation (adding two numbers). The reason is that the target of the AsyncCallback delegate (AddComplete() in this example) does not have access to the original BinaryOp delegate created in the scope of Main(), and therefore you can’t call EndInvoke() from within AddComplete()!
While you could simply declare the BinaryOp variable as a static member variable in the class to allow both methods to access the same object, a more elegant solution is to use the incoming IAsyncResult parameter.
The incoming IAsyncResult parameter passed into the target of the AsyncCallback delegate is actually an instance of the AsyncResult class (note the lack of an I prefix) defined in the System. Runtime.Remoting.Messaging namespace. The static AsyncDelegate property returns a reference to the original asynchronous delegate that was created elsewhere.
Therefore, if you wish to obtain a reference to the BinaryOp delegate object allocated within Main(), simply cast the System.Object returned by the AsyncDelegate property into type BinaryOp. At this point, you can trigger EndInvoke() as expected:
// Don't forget to import // System.Runtime.Remoting.Messaging! static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete"); // Now get the result. AsyncResult ar = (AsyncResult)itfAR; BinaryOp b = (BinaryOp)ar.AsyncDelegate; Console.WriteLine("10 + 10 is {0}.", b.EndInvoke(itfAR)); isDone = true; }
The final aspect of asynchronous delegates you need to address is the final argument to the BeginInvoke() method (which has been null up to this point). This parameter allows you to pass additional state information to the callback method from the primary thread. Because this argument is prototyped as a System.Object, you can pass in any type of data whatsoever, as long as the callback method knows what to expect. Assume for the sake of demonstration that the primary thread wishes to pass in a custom text message to the AddComplete() method:
static void Main(string[] args) { ... IAsyncResult iftAR = b.BeginInvoke(10, 10, new AsyncCallback(AddComplete), "Main() thanks you for adding these numbers."); ... }
To obtain this data within the scope of AddComplete(), make use of the AsyncState property of the incoming IAsyncResult parameter. Notice that an explicit cast will be required; therefore the primary and secondary threads must agree on the underlying type returned from AsyncState.
static void AddComplete(IAsyncResult itfAR) { ... // Retrieve the informational object and cast it to string. string msg = (string)itfAR.AsyncState; Console.WriteLine(msg); isDone = true; }
Here is the output of the final iteration:
***** AsyncCallbackDelegate Example ***** Main() invoked on thread 1. Add() invoked on thread 3. Working.... Working.... Working.... Working.... Working.... AddComplete() invoked on thread 3. Your addition is complete 10 + 10is 20. Main() thanks you for adding these numbers. Working....
Now that you understand how a .NET delegate can be used to automatically spin off a secondary thread of execution to handle an asynchronous method invocation, let’s turn attention to directly interacting with threads using the System.Threading namespace.
Source Code The AsyncCallbackDelegate project is located under the Chapter 19 subdirectory.